// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "entities/WorldJunction.h"
#include "entities/WorldRoad.h"

#include <unordered_set>

/**
* Base class for iterating through the road network.
* https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
*/
template<typename DerivedClass>
class FWorldNetIteratorBase
{
public:

	bool HasNext() const
	{
		return !_pending.empty();
	}

	void Next()
	{
		// Pop the last element. (depth first)
		FWorldConnectable::Ptr connectable = PopNotVisited();
		if (!connectable) return;

		// Mark as visited.
		MarkVisited(connectable);

		// Handle case by case.
		if (connectable->IsA<FWorldRoad>())
		{
			// Visit a road.
			HandleRoad(connectable->As<FWorldRoad>());
		}
		else if (connectable->IsA<FWorldJunction>())
		{
			// Visit a junction.
			HandleJunction(connectable->As<FWorldJunction>());
		}
		else
		{
			throw std::invalid_argument("Unknown connectable type in input network");
		}
	}

	void Process()
	{
		while (HasNext()) Next();
	}

	void Reset()
	{
		static_cast<DerivedClass*>(this)->OnReset();
		_pending.clear();
		_visited.clear();
	}

	void Reset(const std::vector<FWorldRoad::Ptr>& roads)
	{
		Reset();
		_pending.insert(_pending.end(), roads.begin(), roads.end());
	}

	void Reset(const FWorldRoad::Ptr& road)
	{
		Reset();
		_pending.push_back(road);
	}

	bool IsVisited(const FWorldConnectable::Ptr& connectable) const
	{
		return _visited.find(connectable) != _visited.end();
	}

	void MarkVisited(const FWorldConnectable::Ptr& connectable)
	{
		_visited.insert(connectable);
	}

	FWorldConnectable::Ptr PopNotVisited()
	{
		// Pop until an unvisited connectable is found.
		while (HasNext())
		{
			FWorldConnectable::Ptr connectable = _pending.back();
			_pending.pop_back();
			if (!IsVisited(connectable)) return connectable;
		}
		return nullptr;
	}

	void PushPendingVisit(const FWorldConnectable::Ptr& connectable)
	{
		if (!IsVisited(connectable))
		{
			_pending.push_back(connectable);
		}
	}

	void HandleRoad(const FWorldRoad::Ptr& road)
	{
		static_cast<DerivedClass*>(this)->OnRoad(road);

		if (road->Link)
		{
			auto& predecessor = road->Link->Predecessor;
			if (predecessor)
			{
				HandleRoadLink(road, predecessor);
			}

			auto& successor = road->Link->Successor;
			if (successor)
			{
				HandleRoadLink(road, successor);
			}
		}
	}

	void HandleRoadLink(const FWorldRoad::Ptr& road, const FWorldConnector::Ptr& link)
	{
		FWorldConnectable::Ptr connectable = *link->ElementId;
		if (connectable)
		{
			if (connectable->IsA<FWorldRoad>())
			{
				static_cast<DerivedClass*>(this)->OnRoadRoad(
					road,
					connectable->As<FWorldRoad>(),
					link->ContactPoint
				);
			}
			else if (connectable->IsA<FWorldJunction>())
			{
				static_cast<DerivedClass*>(this)->OnRoadJunction(
					road,
					connectable->As<FWorldJunction>(),
					link->ContactPoint
				);
			}
			else
			{
				throw std::invalid_argument("Unknown connectable type in input network");
			}
			PushPendingVisit(connectable);
		}
	}

	void HandleJunction(const FWorldJunction::Ptr& junction)
	{
		static_cast<DerivedClass*>(this)->OnJunction(junction);

		for (auto& connection : junction->Connection)
		{
			FWorldRoad::Ptr incoming = *connection->IncomingRoad;
			if (incoming)
			{
				PushPendingVisit(incoming);
			}

			FWorldRoad::Ptr connecting = *connection->ConnectingRoad;
			if (connecting)
			{
				PushPendingVisit(connecting);
			}
		}
	}

private:

	void OnReset() {}

	void OnRoad(const FWorldRoad::Ptr&) {}
	void OnJunction(const FWorldJunction::Ptr&) {}

	void OnRoadRoad(const FWorldRoad::Ptr&, const FWorldRoad::Ptr&, FWorldContactPoint) {}
	void OnRoadJunction(const FWorldRoad::Ptr&, const FWorldJunction::Ptr&, FWorldContactPoint) {}

private:

	std::vector<FWorldConnectable::Ptr>			_pending;
	std::unordered_set<FWorldConnectable::Ptr>	_visited;

};
